﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
using System.Net;
using System.IO;
using System.Threading;
using System.Collections;

namespace Facebook
{
    public class FacebookRequest
    {
        internal const String FACEBOOK_REST_URL = "https://api.facebook.com/restserver.php";
        internal const Int32 DEFAULT_EXECUTE_TIMEOUT = 5000;
        internal const Int32 DEFAULT_SYNC_POLLING_INTERVAL = 10;

        public FacebookRequest(IFacebookContext facebookContext, String methodName, IDictionary<String, Object> args)
        {
            facebookContext.RequestCount++;
            this.Context = facebookContext;
            this.CallId = facebookContext.RequestCount;
            this.MethodName = methodName;
            this.Args = args;
        }

        public IFacebookContext Context { get; set; }

        public String MethodName { get; internal set; }

        public Int64 CallId { get; private set; }

        public IDictionary<String, Object> Args { get; internal set; }

        internal static String ComputeSignature(String secret, IDictionary<String, Object> args)
        {
            var sortedArgs = args.OrderBy(arg => arg.Key);

            var seedBuilder = new StringBuilder();

            foreach (var arg in sortedArgs)
            {
                Object value = null;
                IEnumerable col = null;
                if (arg.Value.TryMakeEnumerable(out col)) value = col.ToDelimitedString(',');
                else value = arg.Value;

                seedBuilder.AppendFormat("{0}={1}", arg.Key, value);
            }

            seedBuilder.Append(secret);

            var seed = seedBuilder.ToString();
            var seedBytes = Encoding.ASCII.GetBytes(seed);
            var sig = MD5.Create().ComputeHashString(seed);

            return sig.ToLower();
        }

        public IAsyncResult BeginExecute(AsyncCallback callback)
        {
            try
            {
                var args = new Dictionary<String, Object>(this.Args ?? new Dictionary<String, Object>());

                args.Add("method", this.MethodName);
                args.Add("api_key", this.Context.ApiKey);
                args.Add("v", this.Context.Version);

                if (this.Context.HasSession)
                {
                    args.Add("ss", "1");
                    args.Add("session_key", this.Context.Session.SessionKey);
                    args.Add("call_id", this.CallId);
                }
                else
                {
                    args.Add("ss", "0");
                    args.Add("session_key", String.Empty);
                    args.Add("call_id", String.Empty);
                }
                var sig = FacebookRequest.ComputeSignature(this.Context.Secret, args);
                args.Add("sig", sig);

                var content = new StringBuilder();         

                foreach (var arg in args)
                {
                    Object value = null;
                    IEnumerable col = null;
                    if (arg.Value.TryMakeEnumerable(out col)) value = col.ToDelimitedString(',');
                    else value = arg.Value;

                    content.AppendFormat("{0}{1}={2}", content.Length > 0 ? "&" : String.Empty, arg.Key, value);
                }

                var request = (HttpWebRequest)WebRequest.Create(FacebookRequest.FACEBOOK_REST_URL);
                request.Method = "POST";
                request.ContentType = "application/x-www-form-urlencoded";
                request.ContentLength = content.Length;

                using (var stream = request.GetRequestStream())
                {
                    using (var writer = new StreamWriter(stream))
                    {
                        writer.Write(content.ToString());
                        writer.Close();
                    }
                }

                var state = new FacebookAsyncState(this, request);

                IAsyncResult result = request.BeginGetResponse(callback, state);
                return result;
            }
            catch (Exception ex) { throw new FacebookException("Error attempting to execute {0} request: {1}", ex, this.MethodName, ex.Message); }
        }

        public static FacebookResponse<TValue> EndExecute<TValue>(IAsyncResult result, String responseXmlRoot)
        {
            FacebookAsyncState state = null;
            try
            {
                state = (FacebookAsyncState)result.AsyncState;
                var response = (HttpWebResponse)state.HttpRequest.EndGetResponse(result);

                String responseContent = null;
                using (var stream = response.GetResponseStream())
                {
                    using (var reader = new StreamReader(stream))
                    {
                        responseContent = reader.ReadToEnd();
                        reader.Close();
                    }
                }

                return new FacebookResponse<TValue>(responseContent, responseXmlRoot);
            }
            catch (WebException ex) { return new FacebookResponse<TValue>(ex); }
            catch (Exception ex) { throw new FacebookException("Error attempting to execute {0} request: {1}", ex, state.Request.MethodName, ex.Message); }
        }

        public FacebookResponse<TValue> Execute<TValue>(String responseXmlRoot)
        {
            return this.Execute<TValue>(FacebookRequest.DEFAULT_EXECUTE_TIMEOUT, responseXmlRoot);
        }

        public FacebookResponse<TValue> Execute<TValue>(Int32 timeout, String responseXmlRoot)
        {
            FacebookResponse<TValue> response = null;
            try
            {
                IAsyncResult executeResult = this.BeginExecute(
                    new AsyncCallback(result =>
                    {
                        try
                        {
                            var state = (FacebookAsyncState)result.AsyncState;
                            response = FacebookRequest.EndExecute<TValue>(result, responseXmlRoot);
                        }
                        catch (Exception ex) { throw new FacebookException("Error attempting to execute {0} request: {1}", ex, this.MethodName, ex.Message); }
                    }));

                if (executeResult.AsyncWaitHandle.WaitOne(timeout))
                {
                    if (!executeResult.IsCompleted)
                    {
                        try { ((FacebookAsyncState)executeResult).HttpRequest.Abort(); }
                        catch (WebException ex)
                        {
                            if (ex.Status == WebExceptionStatus.RequestCanceled)
                            {
                                throw new FacebookRequestException("The request timed out after {0}ms.", ex, timeout);
                            }
                            else throw ex;
                        }
                    }
                }

                return response;
            }
            catch (Exception ex) { return new FacebookResponse<TValue>(ex); }
        }
    }
}